package gobelinmaker.server;

import com.esotericsoftware.kryonet.Connection;
import com.esotericsoftware.kryonet.Listener;
import com.esotericsoftware.kryonet.Server;
import com.esotericsoftware.kryonet.util.InputStreamSender;
import com.pi4j.io.gpio.GpioFactory;
import com.pi4j.io.gpio.Pin;
import com.pi4j.io.gpio.RaspiPin;
import gobelinmaker.data.BasicHoleData;
import gobelinmaker.data.BasicMotorHoleData;
import gobelinmaker.data.HoleFinder;
import gobelinmaker.data.HoleFinderResult;
import gobelinmaker.data.JSONUtils;
import java.awt.Dimension;
import java.awt.geom.AffineTransform;
import java.awt.geom.NoninvertibleTransformException;
import java.awt.geom.Point2D;
import java.awt.image.BufferedImage;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.sql.Timestamp;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import javax.imageio.ImageIO;
import org.imgscalr.Scalr;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;

/**
 * Szerver alaposztálya.
 *
 * @author imruf84
 */
public final class GobelinServer {

    /**
     * Gobelin készítő szerver típus.
     */
    public static String GOBELIN_MAKER_SERVER_TYPE = "GOBELIN_MAKER_SERVER";

    /**
     * Alapértelmezett portok.
     */
    public static int DEFAULT_PORT = 8091;

    /**
     * Motorok azonosítói.
     */
    public static String X_AXIS_MOTOR_CONTROL_ID = "X_AXIS_MOTOR_CONTROL";
    public static String Y_AXIS_MOTOR_CONTROL_ID = "Y_AXIS_MOTOR_CONTROL";
    public static String Z_AXIS_MOTOR_CONTROL_ID = "Z_AXIS_MOTOR_CONTROL";

    /**
     * Kamerák azonosítói.
     */
    public static String CAMERA_KANAVA_ID = "KANAVA";
    public static String CAMERA_RULER_ID = "RULER";

    /**
     * Adatküldés gyakorisága ezredmásodpercben.
     */
    private final int sendDataFreq = 1000;

    /**
     * Motorok utasításai.
     */
    private final LinkedList<StepperMotorAction> motorSteps = new LinkedList<>();
    /**
     * Motorok vezérlése még folyik.
     */
    private AtomicBoolean motorRunning = new AtomicBoolean(false);
    /**
     * Motorok vezérlését végző szál.
     */
    private Thread motorStepsThread = new Thread();
    /**
     * Lyukra igazítás objektuma.
     */
    private AtomicReference<HoleToSnap> holeToSnap = new AtomicReference<>(null);
    /**
     * Lyukra igazítás tűréshatára képpontokban.
     */
    private double snapToHoleTollerance = 1;

    /**
     * Fényforrás.
     */
    private LightControl cameraLight = null;

    /**
     * Addig tart a szerver adatainak küldése, amíg ez igaz.
     */
    private AtomicBoolean serverStateSend = new AtomicBoolean(true);

    /**
     * Szerver állapotának küldését végző szál.
     */
    private Thread serverStateSenderThread = null;

    /**
     * Szerver neve.
     */
    private String name = "";
    /**
     * Szerver típusa.
     */
    private final String type = GobelinServer.GOBELIN_MAKER_SERVER_TYPE;
    /**
     * Szerver portja.
     */
    private int port = -1;
    /**
     * Szerver.
     */
    Server server = new Server();
    /**
     * Szerver címe.
     */
    private InetAddress address = null;

    /**
     * Csatlakozott kliensek.
     */
    private HashMap<Integer, Connection> connections = new HashMap<>();
    /**
     * Kamerák tárolója.
     */
    private HashMap<String, ServerCamera> cameras = new HashMap<>();
    /**
     * Motorvezérlők tárolója.
     */
    private HashMap<String, MotorControl> motorControls = new HashMap<>();

    /**
     * Aktív munkafolyamat.
     */
    private final AtomicReference<ServerJob> currentJob = new AtomicReference<>(null);
    /**
     * Aktuális letapogatott kanava.
     */
    private final AtomicReference<ServerHoles> currentServerHoles = new AtomicReference<>(null);
    /**
     * Munkafolyamatok könyvtára.
     */
    private String workingDir;
    /**
     * Letapogatott kanava adatai.
     */
    private String holesDir;
    /**
     * Beállítások állománya.
     */
    private String optionsFileName = "config.cfg";
    /**
     * Szerver állapota.
     */
    private String state = ServerStates.IDLE;
    /**
     * Elkészültség.
     */
    private Double progress = Double.NaN;
    /**
     * Egy hívásban történő elemek eltávolításának a száma.
     */
    private final AtomicInteger removeJobElementsCount = new AtomicInteger(10);

    /**
     * Konstruktor.
     *
     * @param name szerver neve
     * @param vcameras igaz esetén virtualizálja a kamerákat
     * @param vmotors igaz esetén virtualizálja a motorokat
     * @param vlights igaz esetén virtualizálja a fényeket
     * @throws IOException kivétel
     */
    public GobelinServer(
            String name,
            boolean vcameras,
            final boolean vmotors,
            boolean vlights) throws IOException, Exception {

        this.workingDir = JarPath.getJarContainingFolder(GobelinServer.class) + "/job/";
        this.holesDir = JarPath.getJarContainingFolder(GobelinServer.class) + "/holes/";

        this.name = name;

        // Munkafolyamat létrehozása.
        this.currentJob.set(new ServerJob(this.workingDir, ""));

        // Letapogatott kanava létrehozása.
        this.currentServerHoles.set(new ServerHoles(this.holesDir, false, 0));

        // Szerver létrehozása.
        // Szerver címének tárolása.
        Enumeration<NetworkInterface> n = NetworkInterface.getNetworkInterfaces();
        for (; n.hasMoreElements();) {
            NetworkInterface e = n.nextElement();
            Enumeration<InetAddress> a = e.getInetAddresses();
            for (; a.hasMoreElements();) {
                InetAddress addr = a.nextElement();
                if (!addr.isLoopbackAddress() && addr.isSiteLocalAddress()) {
                    this.address = addr;
                }
            }
        }

        // Ha nem találtunk megfelelő címet, akkor hibával kilépünk.
        if (null == this.address) {
            System.err.println("IP cím nem kérdezhető le.");
            System.exit(1);
        }

        // Osztályok regisztrálása.
        Network.register(this.server);

        this.server.start();
        this.port = GobelinServer.DEFAULT_PORT;
        this.server.bind(port, port);

        // Események kezelése.
        this.server.addListener(new Listener() {
            @Override
            public void connected(Connection connection) {
            }

            @Override
            public void disconnected(Connection connection) {
                if (getConnections().containsKey(connection.getID())) {
                    System.out.println("SERVER:client disconnected");
                }
                getConnections().remove(connection.getID());
            }

            @Override
            public void received(final Connection connection, Object o) {

                // Adatok kérése a szerverről.
                if (o instanceof DiscoveryRequest) {
                    DiscoveryResponse drp = new DiscoveryResponse();
                    drp.serverName = getServerName();
                    drp.serverType = getServerType();
                    drp.serverPort = getServerPort();
                    drp.serverAddress = getServerAddress().getHostAddress();
                    connection.sendTCP(drp);
                    connection.close();
                    return;
                }

                // Csatlakozási kérelem.
                if (o instanceof ConnectionRequest) {

                    // Kliens eltárolása az állandó kliensek közé.
                    getConnections().put(connection.getID(), connection);
                    System.out.println("SERVER:client connected");
                    connection.sendTCP(new ConnectionResponse());

                    // Szerveradatok küldése az új csatlakozóhoz.
                    sendServerStateData();

                    return;
                }

                // Kamera kép igénylése.
                if (o instanceof CameraImageRequest) {
                    CameraImageRequest cir = (CameraImageRequest) o;
                    // Kamera lekérdezése.
                    ServerCamera sc = getCamera(cir.cameraID);
                    // Ha létezik a kamera, akkor feliratjuk a klienst a következő kép küldésére.
                    if (null != sc) {
                        // Ha még nincs hozzáadva a kapcsolat, akkor megtesszük.
                        if (!sc.contains(connection)) {
                            sc.add(connection);
                        }
                    }

                    return;
                }

                // Kamerák listájának a kérése.
                if (o instanceof CamerasListRequest) {
                    CamerasListResponse clr = new CamerasListResponse();
                    for (String ID : getCameras().keySet()) {
                        clr.put(ID, getCameras().get(ID).getCameraName());
                    }

                    // Lista visszaküldése.
                    connection.sendTCP(clr);

                    return;
                }

                // Motor vezérlése.
                if (o instanceof MotorControlRequest) {
                    final MotorControlRequest mc = (MotorControlRequest) o;

                    handleMotorRequest(mc, true);

                    return;
                }

                // Kanava letapogatásának a leállítása.
                if (o instanceof StopDiscoveringHolesRequest) {

                    holeToSnap.set(null);
                    setServerState(ServerStates.IDLE);

                    return;
                }

                // Letapogatott lyuk kérése.
                if (o instanceof DiscoveredHoleRequest) {
                    final DiscoveredHoleRequest dhr = (DiscoveredHoleRequest) o;

                    // Adott pont lekérdezése.
                    BasicMotorHoleData bhd = currentServerHoles.get().getHole(dhr.holeIndex);

                    // Ha találtunk lyukat akkor elküldjük.
                    DiscoveredHoleResponse dhrp = new DiscoveredHoleResponse();
                    if (null != bhd) {
                        dhrp.holeIndex = dhr.holeIndex;
                        dhrp.holeData = bhd;
                    }

                    // Adatok visszaküldése.
                    connection.sendTCP(dhrp);

                    return;
                }

                // Tű kalibrációs pontjának a megváltoztatásának a kérése.
                if (o instanceof NeedleCalibrationPointChangeRequest) {

                    NeedleCalibrationPointChangeRequest ncpcr = (NeedleCalibrationPointChangeRequest) o;
                    ServerCamera sc = getCamera(ncpcr.cameraID);

                    // Ha van ilyen kamer, akkor megváltoztatjuk a kért adatot.
                    if (null != sc) {
                        sc.setNeedleCalibrationPoint(ncpcr.p);
                        storeServerOptionsToFile(optionsFileName);
                    }

                    return;
                }

                // Kamera kalibrációs pontjának a megváltoztatásának a kérése.
                if (o instanceof CameraCalibrationPointsChangeRequest) {

                    CameraCalibrationPointsChangeRequest ccpcr = (CameraCalibrationPointsChangeRequest) o;
                    ServerCamera sc = getCamera(ccpcr.cameraID);

                    // Ha van ilyen kamera, akkor megváltoztatjuk a kért adatot.
                    if (null != sc) {
                        sc.setCameraCalibrationPoints(ccpcr.pTL, ccpcr.pTR, ccpcr.pBR, ccpcr.pBL);
                        storeServerOptionsToFile(optionsFileName);
                    }

                    return;
                }

                // Kamera színkezelésével kapcsolatos kérés.
                if (o instanceof CameraColorAdjustmentChangeRequest) {

                    CameraColorAdjustmentChangeRequest bccr = (CameraColorAdjustmentChangeRequest) o;
                    ServerCamera sc = getCamera(bccr.cameraID);

                    // Ha van ilyen kamera, akkor megváltoztatjuk a kért adatot.
                    if (null != sc) {
                        sc.setBrightness(bccr.brightness);
                        sc.setContrast(bccr.contrast);
                        sc.setFocus(bccr.focus);
                        storeServerOptionsToFile(optionsFileName);
                    }

                    return;
                }

                // Fényforrás vezérlése.
                if (o instanceof LightControlRequest) {

                    LightControlRequest lcr = (LightControlRequest) o;

                    // Fényforrás állapotának frissítése.
                    if (lcr.on) {
                        cameraLight.turnOn();
                    } else {
                        cameraLight.turnOff();
                    }

                    return;
                }

                // Munkafolyamattal kapcsolatos kérés.
                if (o instanceof JobRequest) {

                    JobRequest jr = (JobRequest) o;

                    if (!(null == jr.woolData) || !(null == jr.holeData)) {
                        try {
                            // Ha a munkamenettel kapcsolatos adat érkezett.
                            synchronized (currentJob) {
                                currentJob.get().storeWool(jr.woolData);
                                for (BasicHoleData bhd : jr.holeData) {
                                    currentJob.get().storeHole(bhd);
                                }
                            }

                            progress = (((double) jr.total - (double) jr.left) / (double) jr.total) * 100.;

                            JobResponse jrs = new JobResponse();
                            jrs.dataNeeded = true;
                            connection.sendTCP(jrs);
                        } catch (IOException ex) {
                            // Hiba visszaküldése.
                            JobResponse jrs = new JobResponse();
                            jrs.hasError = true;
                            jrs.errorMessage = ex.getLocalizedMessage();
                            connection.sendTCP(jrs);
                            return;
                        }

                    }

                    // Új munkafolyamat indítása.
                    if (jr.create) {
                        // Ha nem foglalt a szerver, akkor elindítjuk.
                        try {
                            synchronized (currentJob) {
                                if (currentJob.get().isFinished()) {

                                    currentJob.set(new ServerJob(workingDir, jr.jobName));
                                    JobResponse jrs = new JobResponse();
                                    jrs.jobCreated = true;
                                    jrs.dataNeeded = true;
                                    connection.sendTCP(jrs);

                                    progress = .0;
                                    setServerState(ServerStates.JOB_CREATE);

                                    return;

                                } else {
                                    // A szerver már foglalt.
                                    JobResponse jrs = new JobResponse();
                                    jrs.hasError = true;
                                    jrs.errorMessage = "A szerver jelenleg foglalt.";
                                    connection.sendTCP(jrs);
                                    return;
                                }
                            }

                        } catch (IOException ex) {
                            // Hiba visszaküldése.
                            JobResponse jrs = new JobResponse();
                            jrs.hasError = true;
                            jrs.errorMessage = ex.getLocalizedMessage();
                            connection.sendTCP(jrs);
                            return;
                        }

                    }

                    // Ha a munkamenet adatainak küldése befejeződött.
                    if (jr.finished) {

                        progress = 100.;
                        setServerState(ServerStates.IDLE);

                        JobResponse jrs = new JobResponse();
                        jrs.jobStored = true;
                        connection.sendTCP(jrs);
                        return;
                    }

                    // Munkamenet törlésének a kérelme.
                    if (jr.remove) {

                        final AtomicBoolean finished = new AtomicBoolean(false);
                        final AtomicInteger total = new AtomicInteger(0);
                        final AtomicInteger removed = new AtomicInteger(0);

                        total.set(currentJob.get().getFilesCount());

                        // A törlést külön szálban indítjuk.
                        new Thread(new Runnable() {
                            @Override
                            public void run() {

                                progress = .0;
                                setServerState(ServerStates.JOB_REMOVE);
                                remove();
                                setServerState(ServerStates.IDLE);
                            }

                            public void remove() {
                                int lRemoved;
                                synchronized (currentJob) {
                                    if (null == currentJob.get()) {
                                        setServerState(ServerStates.IDLE);
                                        finished.set(true);
                                        return;
                                    }

                                    lRemoved = currentJob.get().removeNext(removeJobElementsCount.get());
                                    removed.set(removed.get() + lRemoved);
                                }

                                if (!(0 < lRemoved)) {
                                    JobResponse jrs = new JobResponse();
                                    jrs.jobRemoved = true;
                                    connection.sendTCP(jrs);
                                    setServerState(ServerStates.IDLE);
                                    finished.set(true);
                                } else {
                                    remove();
                                }

                            }

                        }).start();

                        // Törlés alatt is küldjük a statisztikákat.
                        new Thread(new Runnable() {
                            @Override
                            public void run() {
                                try {
                                    sendData();
                                } catch (InterruptedException | IOException ex) {
                                    System.err.println(ex.getLocalizedMessage());
                                }
                            }

                            public void sendData() throws InterruptedException, IOException {
                                progress = ((double) removed.get() / (double) total.get()) * 100.;
                                sendServerStateData();
                                Thread.sleep(sendDataFreq);
                                if (!finished.get()) {
                                    sendData();
                                }
                            }
                        }).start();

                        return;
                    }

                    return;
                }
            }
        });

        // Kamera fényforrásának létrehozása.
        this.cameraLight = new LightControl(vlights ? null : RaspiPin.GPIO_08);
        // Motorvezérlők létrehozása.
        this.createMotorControls(vmotors);
        // A kamerák képeinek készítését/küldését külön szálakban kezeljük.
        this.createCameras(vcameras);

        // Szerevr állapotának küldésének a szála.
        createServerStateSenderThread();

        // Leállítás eseménye.
        final Thread t = Thread.currentThread();
        Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
            @Override
            public void run() {

                stopMotors(vmotors);
                close();

                // Adatküldő szál leállítása.
                serverStateSend.set(false);

                // Várunk, amíg minden szál befejezi a futását.
                try {
                    t.join();
                    serverStateSenderThread.join();
                } catch (InterruptedException ex) {
                    System.err.println(ex.getLocalizedMessage());
                }

                System.out.println("server stopped");
            }
        }));

        // Szerver beállításainak a beolvasása.
        // Ha létezik az állomány...
        String fn = JarPath.getJarContainingFolder(GobelinServer.class) + File.separator + optionsFileName;
        if (new File(fn).exists()) {
            // ...akkor beolvassuk a tartalmát.
            System.out.println("Loading server options from " + fn + "...");
            this.loadServerOptionsFromFile(fn);
        } else {
            // ...egyébként tároljuk a szerver alapértelmezett beállításait.
            this.storeServerOptionsToFile(fn);
        }

        System.out.println(
                "Server is listening at port " + port
                + " [IP:" + this.getServerAddress() + "]...");

    }

    /**
     * Motorvezérléssel kapcsolatos parancsok kezelése.
     *
     * @param mc motorvezérlés kérése
     * @param removeOldData igaz esetén új adatcsomag jön létre
     */
    private void handleMotorRequest(final MotorControlRequest mc, final boolean removeOldData) {
        // Motor vezérlés tárolása.
        synchronized (motorSteps) {

            // Lyukra igazítás esetén lefuttatjuk az adott irányt többször, hogy a tű elkerüljön a lyukról ha esetlegesen már ott lenne.
            long stepsFactor = (mc.snap ? 5 : 1);

            if (0 != mc.steps.x) {
                motorSteps.add(new StepperMotorAction(GobelinServer.X_AXIS_MOTOR_CONTROL_ID, stepsFactor * mc.steps.x, mc.continuous.x));
            }
            if (0 != mc.steps.y) {
                motorSteps.add(new StepperMotorAction(GobelinServer.Y_AXIS_MOTOR_CONTROL_ID, stepsFactor * mc.steps.y, mc.continuous.y));
            }
            if (0 != mc.steps.z) {
                motorSteps.add(new StepperMotorAction(GobelinServer.Z_AXIS_MOTOR_CONTROL_ID, stepsFactor * mc.steps.z, mc.continuous.z));
            }

            // Lyukra igazítás esetén tároljuk a szükséges dolgokat is.
            if (mc.snap) {
                addMotorEvent(new Runnable() {
                    @Override
                    public void run() {

                        // Ha nincs szükség új adat létrehozására, akkor kilépünk.
                        if (!removeOldData) {
                            return;
                        }

                        HoleToSnap hts = new HoleToSnap(mc);

                        // Letapogatás módra állítás ha szükséges.
                        if (!(0 > mc.discover[0]) || !(0 > mc.discover[1])) {
                            progress = .0;
                            setServerState(ServerStates.DISCOVER_HOLES);
                            hts.toDiscoverMode(mc.discover[0], mc.discover[1]);

                            // Motorok nullázása.
                            getXMotorControl().setSteps(0);
                            getYMotorControl().setSteps(0);

                            try {
                                currentServerHoles.set(new ServerHoles(holesDir, true, mc.discover[1]));
                            } catch (IOException ex) {
                                System.err.println(ex.getLocalizedMessage());
                            }
                        }

                        holeToSnap.set(hts);
                    }
                });
            }

            // Parancsok végrehajtása.
            doMotorSteps();
        }
    }

    /**
     * Szerver címének a lekérdezése.
     *
     * @return szerver címe
     */
    public InetAddress getServerAddress() {
        return this.address;
    }

    /**
     * Szerver állapotának frissítésének a küldését végző szál létrehozása.
     */
    private void createServerStateSenderThread() {

        this.serverStateSenderThread = new Thread(new Runnable() {
            @Override
            public void run() {
                while (serverStateSend.get()) {
                    sendServerStateData(new Runnable() {
                        @Override
                        public void run() {
                            try {
                                Thread.sleep(sendDataFreq);
                            } catch (InterruptedException ex) {
                                System.err.println(ex.getLocalizedMessage());
                            }
                        }
                    });
                }
            }
        });

        this.serverStateSenderThread.start();
    }

    /**
     * Kamerák elindítása.
     *
     * @param virtual igaz esetén virtuális kamerák készülnek
     */
    private void createCameras(boolean virtual) throws UnsupportedEncodingException, Exception {

        if (!virtual) {
            // Webkamera képkészítő szkript létrehozása.
            GobelinRealCamera.createShellScriptFileForCaptureFocusedImages();
        }

        // A kanava kamera fejjel lefelé áll, így elforgatjuk a képét 180 fokkal.
        AffineTransform tr = new AffineTransform();
        tr.rotate(Math.toRadians(180));
        final ServerCamera sc = new ServerCamera(GobelinServer.CAMERA_KANAVA_ID, "Kanava kamera", this, tr, 0, new Dimension(640, 480), -1) {
            @Override
            public void newImageCreated(final BufferedImage image) {

                // Tű kalibráció adata.
                NeedleCalibrationPointResponse ncr = this.needleCalibrationDataToResponse();

                // Kamera kalibrációs adata.
                CameraCalibrationPointsResponse ccr = this.cameraCalibrationDataToResponse();

                // Kamera színadatai.
                CameraColorAdjustmentResponse ccar = this.cameraColorAdjustmentDataToResponse();

                // Kép átalakítása
                final ByteArrayOutputStream output = new ByteArrayOutputStream();
                try {
                    synchronized (image) {
                        ImageIO.write(image, "jpg", output);
                        output.flush();
                        output.close();
                    }

                    // Kép küldése (darabokban).
                    // Küldendő adat;
                    byte data[] = output.toByteArray();

                    // Darabszám.
                    int partSize = 512;
                    int pc = (int) Math.ceil((double) data.length / (double) partSize);
                    final int partsCount = 0 == pc ? 1 : pc;
                    // Azonosító.
                    final String ID = "" + new Timestamp(new Date().getTime()).getTime();

                    // A kamerára feliratkozott kliensek lekérdezése.
                    Object connections[] = this.toArray();
                    this.clear();
                    ByteArrayInputStream input = new ByteArrayInputStream(data);
                    for (Object co : connections) {
                        Connection c = (Connection) co;

                        // Ha nincs kapcsolat, akkor nem teszünk semmit.
                        if ((null == c) || !c.isConnected()) {
                            continue;
                        }

                        // Tű pozíciójának küldése.
                        c.sendTCP(ncr);

                        // Kamera kalibrációs adatainak a küldése.
                        c.sendTCP(ccr);

                        // Kamerabeállítások adatainak a küldése.
                        c.sendTCP(ccar);

                        // Kamerakép küldése darabokban.
                        c.addListener(new InputStreamSender(input, partSize) {

                            // Számláló.
                            AtomicInteger counter = new AtomicInteger(0);

                            @Override
                            protected void start() {
                            }

                            @Override
                            protected Object next(byte[] bytes) {

                                ByteArrayPartResponse barp = new ByteArrayPartResponse();
                                barp.ID = ID;
                                barp.count = partsCount;
                                barp.part = counter.incrementAndGet();
                                barp.data = bytes;

                                return barp;
                            }
                        });
                    }

                } catch (IOException | NullPointerException | IndexOutOfBoundsException ex) {
                    System.err.println(ex.getLocalizedMessage());
                }

                // Lyukra ugrás módban létrehozzuk a megfelelő vezérlést ha szükséges és nincs épp motorvezérlés sem most sem a jövőben.
                if (!(null == holeToSnap.get()) && !motorRunning.get() && !(0 < motorSteps.size())) {

                    final HoleToSnap hts = holeToSnap.get();
                    MotorControlRequest mcr = hts.mcr;

                    try {

                        // Kamerakép transzformálása.
                        BufferedImage img;
                        synchronized (image) {
                            img = ServerCamera.transformImage(image, this.getCameraCalibrationPoints());
                        }

                        // Minden esetben ezsköztől függetlenül ugyanazzal a képmérettel dolgozunk.
                        img = Scalr.resize(img, Scalr.Method.ULTRA_QUALITY, 200, Scalr.OP_ANTIALIAS);

                        // Lyukak megkeresése.
                        HoleFinder hf = new HoleFinder();
                        HoleFinderResult hfr = hf.findHoles(img);

                        // Tű helye.
                        Point2D np = new Point2D.Double(getNeedleCalibrationPoint().getX() * img.getWidth(), getNeedleCalibrationPoint().getY() * img.getHeight());

                        // Legközelebbi lyuk megkeresése.
                        Point2D pMin = hfr.getCalculatedPoints()[0][0];
                        double dMin = pMin.distance(np);
                        for (Point2D[] pp : hfr.getCalculatedPoints()) {
                            for (Point2D p : pp) {

                                // Irány esetében elhagyjuk a mögötte lévő pontokat.
                                if ((mcr.snap) && (0 < mcr.steps.x) && (p.getX() < np.getX())) {
                                    continue;
                                }
                                if ((mcr.snap) && (0 > mcr.steps.x) && (p.getX() > np.getX())) {
                                    continue;
                                }
                                if ((mcr.snap) && (0 < mcr.steps.y) && (p.getY() < np.getY())) {
                                    continue;
                                }
                                if ((mcr.snap) && (0 > mcr.steps.y) && (p.getY() > np.getY())) {
                                    continue;
                                }

                                // Vizsgálat elvégzése.
                                double d = p.distance(np);
                                if (d < dMin) {
                                    pMin = p;
                                    dMin = d;
                                }
                            }
                        }

                        // Eltávolodás mértéke.
                        double d = 10;

                        // Ha valamilyen oknál fogva az új távolság meghaladja a legutóbbit, akkor a tű túlment a célon.
                        if (((hts.mcr.steps.x != 0) || (hts.mcr.steps.y != 0)) && (0 < hts.getLastDistance()) && (Math.abs(hts.getLastDistance() - dMin) > d)) {
                            // Ebben az esetben iránymentesen igazítjuk.
                            hts.mcr.steps.x = 0;
                            hts.mcr.steps.y = 0;
                        } else {
                            // Új távolság tárolása.
                            hts.setLastDistance(dMin);
                        }

                        // Irány megadása (ha tűréshatáron kívül van a távolság).
                        boolean xIsOk = true;
                        if (snapToHoleTollerance < Math.abs(pMin.getX() - np.getX())) {
                            xIsOk = false;
                            motorSteps.add(new StepperMotorAction(GobelinServer.X_AXIS_MOTOR_CONTROL_ID, np.getX() < pMin.getX() ? 1 : -1, false));
                        } else {
                            motorSteps.add(new StepperMotorAction(GobelinServer.X_AXIS_MOTOR_CONTROL_ID, 0, false));
                        }

                        boolean yIsOk = true;
                        if (snapToHoleTollerance < Math.abs(pMin.getY() - np.getY())) {
                            yIsOk = false;
                            motorSteps.add(new StepperMotorAction(GobelinServer.Y_AXIS_MOTOR_CONTROL_ID, np.getY() < pMin.getY() ? 1 : -1, false));
                        } else {
                            motorSteps.add(new StepperMotorAction(GobelinServer.Y_AXIS_MOTOR_CONTROL_ID, 0, false));
                        }

                        // Ha mindkét oldalról a tűréshetáron belül vagyunk...
                        if (xIsOk && yIsOk) {
                            // ...akkor végeztünk...
                            holeToSnap.set(null);

                            // A biztonság kedvéért méegyszer lefuttatjuk a legutolsó parancsokat.
                            long stepsFactor = 1;
                            motorSteps.add(new StepperMotorAction(GobelinServer.X_AXIS_MOTOR_CONTROL_ID, stepsFactor * getXMotorControl().getLastSteps(), false));
                            motorSteps.add(new StepperMotorAction(GobelinServer.Y_AXIS_MOTOR_CONTROL_ID, stepsFactor * getYMotorControl().getLastSteps(), false));

                            // Majd jelezzük a sikeres lyukra igazodást.
                            addMotorEvent(new Runnable() {
                                @Override
                                public void run() {

                                    // Tárolás esetére létrehozzuk a lyukkal kapcsolatos adatokat is.
                                    BasicMotorHoleData bmhd = new BasicMotorHoleData();
                                    bmhd.holePosition[0] = hts.currentColumn;
                                    bmhd.holePosition[1] = hts.currentRow;
                                    bmhd.motorPosition[0] = getXMotorControl().getCurrentSteps();
                                    bmhd.motorPosition[1] = getYMotorControl().getCurrentSteps();
                                    long holeIndex = (hts.currentRow - 1) * currentServerHoles.get().getColumnsCount() + ((0 != hts.currentRow % 2) ? hts.currentColumn - 1 : currentServerHoles.get().getColumnsCount() - hts.currentColumn);
                                    DiscoveredHoleResponse dhrp = new DiscoveredHoleResponse();
                                    dhrp.holeIndex = holeIndex;
                                    dhrp.holeData = bmhd;

                                    // Következő parancs küldése ha szükséges.
                                    hts.mcr.steps = hts.getNextDirection();
                                    if (null != hts.mcr.steps) {

                                        // Letapogatás mód esetén frissítjük a folyamat állását is.
                                        if (hts.isDiscoverMode()) {
                                            progress = hts.getProgress();

                                            // Valamint tároljuk az új lyuk adatait.
                                            try {
                                                currentServerHoles.get().storeHole(bmhd);

                                                // Majd szétküldjük mindenkinek.
                                                sendToAll(dhrp);

                                            } catch (IOException ex) {
                                                System.err.println(ex.getLocalizedMessage());
                                            }
                                        }

                                        holeToSnap.set(hts);
                                        hts.setLastDistance(-1);
                                        handleMotorRequest(hts.mcr, false);
                                    } else {
                                        // Ha letapogatás módban voltunk, akkor a letapogatásnak is vége.
                                        if (hts.isDiscoverMode()) {
                                            // Valamint tároljuk az új lyuk adatait.
                                            try {
                                                currentServerHoles.get().storeHole(bmhd);

                                                // Majd szétküldjük mindenkinek.
                                                sendToAll(dhrp);

                                            } catch (IOException ex) {
                                                System.err.println(ex.getLocalizedMessage());
                                            }
                                            setServerState(ServerStates.IDLE);
                                        }
                                    }
                                }
                            });

                        }

                    } catch (NoninvertibleTransformException | CloneNotSupportedException ex) {
                        System.err.println(ex.getLocalizedMessage());
                    }

                    doMotorSteps();

                }
            }
        };

        this.cameras.put(sc.getCameraID(), sc);
        sc.start(virtual, sendDataFreq);

        // Mérőszalagok kamerájának a létrehozása.
        final ServerCamera sc1 = new ServerCamera(GobelinServer.CAMERA_RULER_ID, "Vonalzó kamera", this, null, 1, new Dimension(640, 480), 100) {
            @Override
            public void newImageCreated(final BufferedImage image) {

                // Tű kalibráció adata.
                //NeedleCalibrationPointResponse ncr = this.needleCalibrationDataToResponse();
                // Kamera kalibrációs adata.
                CameraCalibrationPointsResponse ccr = this.cameraCalibrationDataToResponse();

                // Kamera színadatai.
                CameraColorAdjustmentResponse ccar = this.cameraColorAdjustmentDataToResponse();

                // Kép átalakítása
                final ByteArrayOutputStream output = new ByteArrayOutputStream();
                try {
                    synchronized (image) {
                        ImageIO.write(image, "jpg", output);
                        output.flush();
                        output.close();
                    }

                    // Kép küldése (darabokban).
                    // Küldendő adat;
                    byte data[] = output.toByteArray();

                    // Darabszám.
                    int partSize = 512;
                    int pc = (int) Math.ceil((double) data.length / (double) partSize);
                    final int partsCount = 0 == pc ? 1 : pc;
                    // Azonosító.
                    final String ID = "" + new Timestamp(new Date().getTime()).getTime();

                    // A kamerára feliratkozott kliensek lekérdezése.
                    Object connections[] = this.toArray();
                    this.clear();
                    ByteArrayInputStream input = new ByteArrayInputStream(data);
                    for (Object co : connections) {
                        Connection c = (Connection) co;

                        // Ha nincs kapcsolat, akkor nem teszünk semmit.
                        if ((null == c) || !c.isConnected()) {
                            continue;
                        }

                        // Tű pozíciójának küldése.
                        //c.sendTCP(ncr);
                        // Kamera kalibrációs adatainak a küldése.
                        c.sendTCP(ccr);

                        // Kamerabeállítások adatainak a küldése.
                        c.sendTCP(ccar);

                        // Kamerakép küldése darabokban.
                        c.addListener(new InputStreamSender(input, partSize) {

                            // Számláló.
                            AtomicInteger counter = new AtomicInteger(0);

                            @Override
                            protected void start() {
                            }

                            @Override
                            protected Object next(byte[] bytes) {

                                ByteArrayPartResponse barp = new ByteArrayPartResponse();
                                barp.ID = ID;
                                barp.count = partsCount;
                                barp.part = counter.incrementAndGet();
                                barp.data = bytes;

                                return barp;
                            }
                        });
                    }

                } catch (IOException | NullPointerException | IndexOutOfBoundsException ex) {
                    System.err.println(ex.getLocalizedMessage());
                }
            }
        };

        this.cameras.put(sc1.getCameraID(), sc1);
        sc1.start(virtual, sendDataFreq);
    }

    /**
     * Motorvezérlők létrehozása.
     *
     * @param virtual igaz esetén virtuális motorok készülnek
     */
    private void createMotorControls(boolean virtual) {

        // Virtuális motorok kellenek.
        if (virtual) {
            this.getMotorControls().put(
                    GobelinServer.X_AXIS_MOTOR_CONTROL_ID,
                    new VirtualMotorControl(GobelinServer.X_AXIS_MOTOR_CONTROL_ID, -1, -100));
            this.getMotorControls().put(
                    GobelinServer.Y_AXIS_MOTOR_CONTROL_ID,
                    new VirtualMotorControl(GobelinServer.Y_AXIS_MOTOR_CONTROL_ID, -1, -100));
            this.getMotorControls().put(
                    GobelinServer.Z_AXIS_MOTOR_CONTROL_ID,
                    new VirtualMotorControl(GobelinServer.Z_AXIS_MOTOR_CONTROL_ID, 1, 0));

            return;
        }

        // Valódi motorok kellenek.
        this.getMotorControls().put(GobelinServer.X_AXIS_MOTOR_CONTROL_ID,
                new DoubleStepperMotorControl(
                        GobelinServer.X_AXIS_MOTOR_CONTROL_ID,
                        new Pin[]{RaspiPin.GPIO_01, RaspiPin.GPIO_04, RaspiPin.GPIO_05, RaspiPin.GPIO_06},
                        -1));
        this.getMotorControls().put(GobelinServer.Y_AXIS_MOTOR_CONTROL_ID,
                new DoubleStepperMotorControl(
                        GobelinServer.Y_AXIS_MOTOR_CONTROL_ID,
                        new Pin[]{RaspiPin.GPIO_07, RaspiPin.GPIO_00, RaspiPin.GPIO_02, RaspiPin.GPIO_03},
                        1));
        this.getMotorControls().put(GobelinServer.Z_AXIS_MOTOR_CONTROL_ID,
                new DoubleStepperMotorControl(
                        GobelinServer.Z_AXIS_MOTOR_CONTROL_ID,
                        new Pin[]{RaspiPin.GPIO_13, RaspiPin.GPIO_14, RaspiPin.GPIO_10, RaspiPin.GPIO_11},
                        1));
    }

    /**
     * Motorok leállítása.
     *
     * @param vmotors igaz esetén virtuális motorok vannak
     */
    private void stopMotors(boolean vmotors) {

        for (MotorControl mc : this.getMotorControls().values()) {
            System.out.println("stop motor:" + mc.getMotorID());
            mc.stop();
        }

        // Tényleges motorok esetén leállítunk minde vezérlőt.
        if (!vmotors) {
            try {

                // Töröljük a hátra lévő utasításokat.
                synchronized (motorSteps) {
                    motorSteps.clear();
                }

                // Megvárjuk az utolsó lépés befejeztét.
                motorStepsThread.join();

            } catch (InterruptedException ex) {
                System.err.println(ex.getLocalizedMessage());
            }

            // Leállítjuk a kütyüt.
            GpioFactory.getInstance().shutdown();
        }
    }

    /**
     * Motorvezérlő esemény hozzáadása.
     */
    private void addMotorEvent(Runnable event) {

        synchronized (motorSteps) {
            motorSteps.add(new StepperMotorEvent(event));
        }

        doMotorSteps();
    }

    /**
     * Motorok vezérlésének elvégzése.
     */
    private void doMotorSteps() {

        // Ha már folyik a vezérlés, akkor nem csinálunk semmit.
        if (motorRunning.get()) {
            return;
        }

        // Parancsok végrehajtása (azért hajtjuk végre szálakban, mert valamiért a hosszú folyamatok után szétkapcsol a szerver).
        this.motorStepsThread = new Thread(new Runnable() {
            @Override
            public void run() {

                boolean doMotorSteps = true;
                StepperMotorAction sma;
                synchronized (motorSteps) {

                    // Ha nincs adat, akkor kilépünk.
                    if (motorSteps.isEmpty()) {
                        return;
                    }

                    // Motor használatban lesz.
                    motorRunning.set(true);

                    // Lekérdezzük és el is távolítjuk a soron következő lépést.
                    sma = motorSteps.remove();

                    // Ha eseményről van szó, akkor azt azonnal feldolgozzuk és ké is lépünk.
                    if (sma instanceof StepperMotorEvent) {
                        doMotorSteps = false;
                        ((StepperMotorEvent) sma).runEvent();
                    } else {

                        // Ha folyamatos a vezérlés, akkor módosítunk néhány dolgot.
                        if (sma.isContinuous()) {
                            // Egyszerre csak egy lépést teszünk meg.
                            sma.setSteps(sma.getSteps() > 0 ? 1 : -1);

                            // Ha van a soron következő parancsok közt nem folyamatos, akkor befejezzük a lépéseket.
                            boolean add = true;
                            for (StepperMotorAction sma2 : motorSteps) {

                                // Ha találtunk ilyet, akkor megjegyezzük kilépünk.
                                if (sma2.getMotorID().equals(sma.getMotorID()) && !sma2.isContinuous()) {
                                    add = false;
                                    break;
                                }
                            }

                            // Ha nem találtunk semmit, akkor folytatjuk a vezérlést.
                            if (add) {
                                motorSteps.add(sma);
                            }

                        }

                    }

                }

                // Lépések végrehajtása (csak ha szükséges).
                if (doMotorSteps) {
                    motorControls.get(sma.getMotorID()).doSteps(sma.getSteps());
                }

                try {
                    // Motorok lépésszámainak a tárolása.
                    storeServerOptionsToFile(JarPath.getJarContainingFolder(GobelinServer.class) + File.separator + optionsFileName);
                } catch (Exception ex) {
                    System.err.println(ex.getLocalizedMessage());
                }

                // Motor már nincs használatban.
                motorRunning.set(false);

                // Szál leállítása.
                motorStepsThread.interrupt();

                // Következő lépés feldolgozása.
                doMotorSteps();

            }
        });

        this.motorStepsThread.start();

    }

    /**
     * Szerverinformációk lekérdezése.
     *
     * @return szerverinformációk objektuma
     */
    public ServerStateResponse createServerStateResponse() {

        ServerStateResponse ssr = new ServerStateResponse();

        // Kamera fényforrásának az állapota.
        ssr.cameraLight = cameraLight.isOn();

        try {
            // Rendszer hőmérséklete.
            Process tempProc = Runtime.getRuntime().exec("cat /sys/class/thermal/thermal_zone0/temp");
            BufferedReader br = new BufferedReader(new InputStreamReader(tempProc.getInputStream()));

            String temp = "", s = "";
            while (null != (s = br.readLine())) {
                temp += s;
            }

            ssr.coreTemperature = Double.parseDouble(temp);

        } catch (IOException ex) {
            // Hőmérséklet sikertelen lekérdezése során nem generálunk hibát.
        }

        // Aktív munkafolyamat neve.
        synchronized (currentJob) {
            if (!(null == this.currentJob.get())) {
                ssr.jobName = this.currentJob.get().getJobName();
            }
        }

        // Szerver állapota.
        ssr.state = this.state;
        ssr.progress = this.progress;

        // Motorokkal kapcsolatos adatok.
        ssr.motorLocations[0] = this.getXMotorControl().getCurrentSteps();
        ssr.motorLocations[1] = this.getXMotorControl().getCurrentMicroSteps();
        ssr.motorLocations[2] = this.getYMotorControl().getCurrentSteps();
        ssr.motorLocations[3] = this.getYMotorControl().getCurrentMicroSteps();

        return ssr;
    }

    /**
     * Szerver állapotának a megadása.
     *
     * @param state új állapot
     */
    private boolean setServerState(String state) {
        this.state = state;

        if (state.equals(ServerStates.IDLE)) {
            progress = Double.NaN;
        }

        // Küldjük a változást.
        this.sendServerStateData();

        return true;
    }

    /**
     * Szerver állapotának küldése.
     */
    public void sendServerStateData() {
        sendServerStateData(null);
    }

    /**
     * Szerver állapotának küldése.
     *
     * @param complete sikeres küldés eseménye
     */
    public void sendServerStateData(Runnable complete) {

        ServerStateResponse ssr = this.createServerStateResponse();

        sendToAll(ssr);

        // Sikeres esemény futtatása.
        if (null != complete) {
            complete.run();
        }
    }

    /**
     * Adat küldése minden kliensnek.
     *
     * @param o adat
     */
    public void sendToAll(Object o) {
        for (Connection c : this.connections.values()) {
            c.sendTCP(o);
        }
    }

    /**
     * Szerver lekérdezése.
     *
     * @return szerver
     */
    private Server getServer() {
        return this.server;
    }

    /**
     * Szerver nevének a lekérdezése.
     *
     * @return szerver neve
     */
    public String getServerName() {
        return this.name;
    }

    /**
     * Szerver típusának a lekérdezése.
     *
     * @return szerver típusa
     */
    public String getServerType() {
        return this.type;
    }

    /**
     * Szerver portjának a lekérdezése.
     *
     * @return szerver portja
     */
    public int getServerPort() {
        return this.port;
    }

    /**
     * Kliensek lekérdezése.
     *
     * @return kliensek
     */
    public HashMap<Integer, Connection> getConnections() {
        return this.connections;
    }

    /**
     * Kamerák lekérdezése.
     *
     * @return kamerák
     */
    private HashMap<String, ServerCamera> getCameras() {
        return this.cameras;
    }

    /**
     * Kamera lekérdezése név alapján.
     *
     * @param cameraID
     * @return kamera
     */
    public ServerCamera getCamera(String cameraID) {
        return this.cameras.get(cameraID);
    }

    /**
     * Motorvezérlők lekérdezése.
     *
     * @return motorvezérlők
     */
    public HashMap<String, MotorControl> getMotorControls() {
        return this.motorControls;
    }

    /**
     * Vízszintes tengely motorvezérlőjének a lekérdezése.
     *
     * @return vízszintes motor vezérlője
     */
    public MotorControl getXMotorControl() {
        return getMotorControls().get(GobelinServer.X_AXIS_MOTOR_CONTROL_ID);
    }

    /**
     * Függőleges tengely motorvezérlőjének a lekérdezése.
     *
     * @return függőleges motor vezérlője
     */
    public MotorControl getYMotorControl() {
        return getMotorControls().get(GobelinServer.Y_AXIS_MOTOR_CONTROL_ID);
    }

    /**
     * Szerver leállítása.
     */
    public void close() {
        this.server.close();
        this.server.stop();
    }

    /**
     * Beállítások tárolása fájlban.
     *
     * @param fileName fájlnév
     */
    public void storeServerOptionsToFile(String fileName) {

        // Szerver neve.
        JSONObject root = new JSONObject();
        root.put("name", this.getServerName());

        // Camerák.
        JSONArray cams = new JSONArray();
        root.put("cams", cams);

        for (ServerCamera sc : this.getCameras().values()) {
            JSONObject cam = new JSONObject();
            cam.put("ID", sc.getCameraID());
            cam.put("ncpX", sc.getNeedleCalibrationPoint().getX());
            cam.put("ncpY", sc.getNeedleCalibrationPoint().getY());

            int i = 0;
            for (Point2D cp : sc.getCameraCalibrationPoints()) {
                cam.put("cp" + i + "X", cp.getX());
                cam.put("cp" + i + "Y", cp.getY());
                i++;
            }

            cam.put("bright", sc.getBrightness());
            cam.put("contr", sc.getContrast());
            cam.put("focus", sc.getFocus());

            cams.add(cam);
        }

        // Motorok.
        root.put("mx", this.getXMotorControl().getCurrentSteps());
        root.put("my", this.getYMotorControl().getCurrentSteps());

        try {
            JSONUtils.JSONToFile(root, fileName);
        } catch (IOException ex) {
            System.err.println(ex.getLocalizedMessage());
        }
    }

    /**
     * Beállítások betöltése fájlból.
     *
     * @param fileName fájlnév
     * @throws java.io.FileNotFoundException kivétel
     */
    public void loadServerOptionsFromFile(String fileName) throws FileNotFoundException {

        JSONObject root = JSONUtils.JSONFromFile(fileName);

        // Szerver neve.
        this.name = (String) root.get("name");

        // Kamerák.
        JSONArray cams = (JSONArray) root.get("cams");
        for (Object oCam : cams.toArray()) {
            JSONObject cam = (JSONObject) oCam;
            ServerCamera sc = this.getCamera((String) cam.get("ID"));

            sc.setNeedleCalibrationPoint(new Point2D.Double((double) cam.get("ncpX"), (double) cam.get("ncpY")));
            sc.setCameraCalibrationPoints(
                    new Point2D.Double((double) cam.get("cp0X"), (double) cam.get("cp0Y")),
                    new Point2D.Double((double) cam.get("cp1X"), (double) cam.get("cp1Y")),
                    new Point2D.Double((double) cam.get("cp2X"), (double) cam.get("cp2Y")),
                    new Point2D.Double((double) cam.get("cp3X"), (double) cam.get("cp3Y")));
            sc.setBrightness(((Double) cam.get("bright")).floatValue());
            sc.setContrast(((Double) cam.get("contr")).floatValue());
            sc.setFocus(((Double) cam.get("focus")).floatValue());
        }

        // Motorok.
        getXMotorControl().setSteps((Long) root.get("mx"));
        getYMotorControl().setSteps((Long) root.get("my"));
    }

}
